
/*
 * Copyright (c) 2010 Sergey N Frolov
 * 
 * See LICENSE file
 */

#include "ngx_javascript.h"

static JSClass ngx_javascript_global_class = {
	"Global",
	JSCLASS_GLOBAL_FLAGS,
	JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
	JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub,
	JSCLASS_NO_OPTIONAL_MEMBERS
};

static JSClass ngx_javascript_server_class = {
	"Server",
	0,
	JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
	JS_EnumerateStub, ngx_javascript_server_resolve, JS_ConvertStub, JS_FinalizeStub,
	JSCLASS_NO_OPTIONAL_MEMBERS
};

static JSClass ngx_javascript_request_class = {
	"Request",
	0,
	JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
	JS_EnumerateStub, ngx_javascript_request_resolve, JS_ConvertStub, JS_FinalizeStub,
	JSCLASS_NO_OPTIONAL_MEMBERS
};

/*
 * Get worker slot from server pool
 */
ngx_javascript_worker_t *ngx_javascript_server_worker_slot(ngx_javascript_server_t *server) {
	int i;
	ngx_javascript_worker_t *worker;
	for (i = 0; i < server->workers_len; i ++) {
		worker = server->workers[i];
		if (worker->client == NULL) return worker;
	}
	if (i < server->workers_max) {
		JSContext *jcx;
		worker = ngx_javascript_calloc(sizeof(ngx_javascript_worker_t));
		if (worker == NULL) return NULL;
		worker->server = server;
		jcx = JS_NewContext(server->jsr, 8192);
		if (jcx == NULL) {
			ngx_javascript_free(worker);
			return NULL;
		}
		worker->jcx = jcx;
		JS_SetOptions(jcx, JSOPTION_VAROBJFIX|JSOPTION_XML);
		JS_SetErrorReporter(jcx, ngx_javascript_global_error);
		JS_SetScriptStackQuota(jcx, server->stack_quota);
		JS_SetOperationCallback(jcx, ngx_javascript_timeout);
		server->workers[i] = worker;
		server->workers_len ++;
		
		// JavaScript functions are simultaneously called from many threads...
		// So, we need to be in request when doing any operations
		JS_SetContextThread(jcx);
		JS_BeginRequest(jcx);
		ngx_javascript_worker_init(worker);
		JS_EndRequest(jcx);
		JS_ClearContextThread(jcx);
		
		return worker;
	}
	return NULL;
}

void ngx_javascript_worker_close(ngx_javascript_worker_t *worker) {
	if (worker->stat_cache != NULL)
		JS_HashTableEnumerateEntries(worker->stat_cache, ngx_javascript_hash_remove_entry, NULL);
	if (worker->link_cache != NULL)
		JS_HashTableEnumerateEntries(worker->link_cache, ngx_javascript_hash_remove_entry, NULL);
	worker->stat_have = worker->link_have = 0;
	worker->stat_data = NULL;
	worker->link_data = NULL;
	while (worker->memory != NULL) {
		worker->endmem = worker->memory->next;
		ngx_javascript_free(worker->memory);
		worker->memory = worker->endmem;
	}
	worker->endmem = NULL;
	JS_SetGlobalObject(worker->jcx, NULL);
}

/*
 * Free memory in worker
 */
void ngx_javascript_worker_free(ngx_javascript_worker_t *worker) {
	ngx_javascript_worker_close(worker);
	if (worker->jcx != NULL) {
		JS_DestroyContextMaybeGC(worker->jcx);
		worker->jcx = NULL;
	}
	if (worker->stat_cache != NULL) JS_HashTableDestroy(worker->stat_cache);
	if (worker->link_cache != NULL) JS_HashTableDestroy(worker->link_cache);
	ngx_javascript_free(worker);
}

/*
 * Resolve $server property
 */
JSBool ngx_javascript_server_resolve(JSContext *jcx, JSObject *obj, jsval idval) {
	if (JSVAL_IS_STRING(idval)) {
		JSString *idstr = JSVAL_TO_STRING(idval);
		char *idbytes = JS_GetStringBytes(idstr);
		char *value;
		ngx_javascript_client_t *client = JS_GetContextPrivate(jcx);
		if (client == NULL) return JS_FALSE;
		value = JS_HashTableLookup(client->env, idbytes);
		if (value != NULL) {
			JSString *s = JS_NewStringCopyZ(jcx, value);
			if (s != NULL) JS_DefineUCProperty(jcx, obj, JS_GetStringChars(idstr), JS_GetStringLength(idstr), STRING_TO_JSVAL(s), NULL, NULL, JSPROP_ENUMERATE);
		}
	}
	return JS_TRUE;
}

/*
 * Resolve $request property
 */
JSBool ngx_javascript_request_resolve(JSContext *jcx, JSObject *obj, jsval idval) {
	if (JSVAL_IS_STRING(idval)) {
		JSString *idstr = JSVAL_TO_STRING(idval);
		size_t idlen = JS_GetStringLength(idstr);
		char *idbytes = JS_GetStringBytes(idstr);
		if (idlen == sizeof("get") - 1 && strcmp(idbytes, "get") == 0) {
			JSObject *global_request_get = JS_NewObject(jcx, NULL, NULL, NULL);
			if (global_request_get == NULL) return JS_FALSE;
			JS_DefineProperty(jcx, obj, "get", OBJECT_TO_JSVAL(global_request_get), NULL, NULL, JSPROP_ENUMERATE);
			ngx_javascript_read_request(jcx, global_request_get, NGX_JAVASCRIPT_PARSE_GET);
		} else if (idlen == sizeof("post") - 1 && strcmp(idbytes, "post") == 0) {
			JSObject *global_request_post = JS_NewObject(jcx, NULL, NULL, NULL);
			if (global_request_post == NULL) return JS_FALSE;
			JS_DefineProperty(jcx, obj, "post", OBJECT_TO_JSVAL(global_request_post), NULL, NULL, JSPROP_ENUMERATE);
			ngx_javascript_read_request(jcx, global_request_post, NGX_JAVASCRIPT_PARSE_POST);
		} else if (idlen == sizeof("cookie") - 1 && strcmp(idbytes, "cookie") == 0) {
			JSObject *global_request_cookie = JS_NewObject(jcx, NULL, NULL, NULL);
			if (global_request_cookie == NULL) return JS_FALSE;
			JS_DefineProperty(jcx, obj, "cookie", OBJECT_TO_JSVAL(global_request_cookie), NULL, NULL, JSPROP_ENUMERATE);
			ngx_javascript_read_request(jcx, global_request_cookie, NGX_JAVASCRIPT_PARSE_COOKIE);
		} else if (idlen == sizeof("files") - 1 && strcmp(idbytes, "files") == 0) {
			// TODO
		}
	}
	return JS_TRUE;
}

/*
 * Init worker. Create global object, classes, functions
 */
int ngx_javascript_worker_init(ngx_javascript_worker_t *worker) {
	JSContext *jcx = worker->jcx;
	JSString *output;
	JSObject *header;
	JSObject *global;
	
	JSObject *global_server;
	JSObject *global_request;
	JSObject *global_scripts;
	JSObject *global_file;
	JSObject *global_line;
	
	global = JS_NewObject(jcx, &ngx_javascript_global_class, NULL, NULL);
	if (global == NULL) return -1;
	JS_SetGlobalObject(jcx, global);
	
	// JavaScript standard classes
	if (JS_InitStandardClasses(jcx, global) == JS_FALSE) return -1;
	
# ifdef NGX_JAVASCRIPT_HAVE_FILE
	ngx_javascript_init_file(jcx, global);
# endif /* NGX_JAVASCRIPT_HAVE_FILE */
	
# ifdef NGX_JAVASCRIPT_HAVE_MYSQL
	ngx_javascript_init_mysql(jcx, global);
# endif /* NGX_JAVASCRIPT_HAVE_MYSQL */
	
# ifdef NGX_JAVASCRIPT_HAVE_SQLITE
	ngx_javascript_init_sqlite(jcx, global);
# endif /* NGX_JAVASCRIPT_HAVE_SQLITE */
	
	// JavaScript global variables and functions
	JS_DefineProperty(jcx, global, "global", OBJECT_TO_JSVAL(global), NULL, NULL, 0);
	JS_DefineFunction(jcx, global, "load", ngx_javascript_global_load, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "include", ngx_javascript_global_load, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "alert", ngx_javascript_global_alert, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "print", ngx_javascript_global_print, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "sleep", ngx_javascript_global_sleep, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "usleep", ngx_javascript_global_usleep, 0, JSPROP_ENUMERATE);
	
	global_server = JS_NewObject(jcx, &ngx_javascript_server_class, NULL, NULL);
	global_request = JS_NewObject(jcx, &ngx_javascript_request_class, NULL, NULL);
	global_scripts = JS_NewObject(jcx, NULL, NULL, NULL);
	
	global_file = JS_NewObject(jcx, NULL, NULL, NULL);
	global_line = JS_NewObject(jcx, NULL, NULL, NULL);
	
	output = JS_NewGrowableString(jcx, NULL, 0);
	header = JS_NewArrayObject(jcx, 0, NULL);
	
	if (output != NULL)
		JS_DefineProperty(jcx, global, "$output", STRING_TO_JSVAL(output), NULL, NULL, JSPROP_ENUMERATE);
	if (header != NULL)
		JS_DefineProperty(jcx, global, "$header", OBJECT_TO_JSVAL(header), NULL, NULL, JSPROP_ENUMERATE);
	
	if (global_server != NULL)
		JS_DefineProperty(jcx, global, "$server", OBJECT_TO_JSVAL(global_server), NULL, NULL, JSPROP_ENUMERATE);
	if (global_request != NULL)
		JS_DefineProperty(jcx, global, "$request", OBJECT_TO_JSVAL(global_request), NULL, NULL, JSPROP_ENUMERATE);
	if (global_scripts != NULL)
		JS_DefineProperty(jcx, global, "$scripts", OBJECT_TO_JSVAL(global_scripts), NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
	
	if (header != NULL) {
		JSString *status_line = JS_NewStringCopyZ(jcx, "200 OK");
		JSString *content_type = JS_NewStringCopyZ(jcx, "text/html; charset=UTF-8");
		JSString *x_powered_by = JS_NewStringCopyZ(jcx, "nginx+javascript/0.2.20100222");
		if (status_line != NULL)
			JS_DefineProperty(jcx, header, "Status", STRING_TO_JSVAL(status_line), NULL, NULL, JSPROP_ENUMERATE);
		if (content_type != NULL)
			JS_DefineProperty(jcx, header, "Content-Type", STRING_TO_JSVAL(content_type), NULL, NULL, JSPROP_ENUMERATE);
		if (x_powered_by != NULL)
			JS_DefineProperty(jcx, header, "X-Powered-By", STRING_TO_JSVAL(x_powered_by), NULL, NULL, JSPROP_ENUMERATE);
		JS_DefineFunction(jcx, header, "toString", ngx_javascript_global_header_toString, 0, 0);
	}
	
	if (global_file != NULL) {
		JS_DefineFunction(jcx, global_file, "toString", ngx_javascript_global_file_string, 0, 0);
		JS_DefineProperty(jcx, global, "__FILE__", OBJECT_TO_JSVAL(global_file), NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
	}
	if (global_line != NULL) {
		JS_DefineFunction(jcx, global_line, "toString", ngx_javascript_global_line_string, 0, 0);
		JS_DefineProperty(jcx, global, "__LINE__", OBJECT_TO_JSVAL(global_line), NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
	}
	
	return 0;
}

/*
 * Execute requested script and return output and header
 */
int ngx_javascript_worker_execute(ngx_javascript_worker_t *worker, int *rstatus, char **routput, size_t *routput_len, char **rheader, size_t *rheader_len) {
	JSBool ok;
	jsval result;
	JSContext *jcx;
	JSString *output = NULL, *header = NULL;
	JSObject *global = NULL;
	size_t output_len = 0, output_len_uc = 0, header_len = 0, header_len_uc = 0;
	jschar *output_str_uc = NULL, *header_str_uc = NULL;
	char *output_str = NULL, *header_str = NULL;
	char realname[PATH_MAX];
	
	*rstatus = 200;
	*rheader = NULL;
	*rheader_len = 0;
	*routput = NULL;
	*routput_len = 0;
	
	worker->client->start = time(NULL);
	
	jcx = worker->jcx;
	
	if (ngx_javascript_realpath(worker, worker->client->script_filename, realname) == NULL) {
		JS_ReportError(jcx, "%s/%s: Load failed", worker->client->document_root == NULL ? "" : worker->client->document_root, worker->client->script_filename);
		return -1;
	}
	
	global = JS_GetGlobalObject(jcx);
	ok = ngx_javascript_execute_script(jcx, global, realname, &result);
	
	// $output
	if (JS_LookupProperty(jcx, global, "$output", &result) == JS_TRUE && JSVAL_IS_STRING(result)) {
		output = JSVAL_TO_STRING(result);
	}
	if (output != NULL) {
		// Output length and bytes
		output_len_uc = JS_GetStringLength(output);
		output_str_uc = JS_GetStringChars(output);
		output_str = JS_GetStringBytes(output);
		JS_EncodeCharacters(jcx, output_str_uc, output_len_uc, NULL, &output_len);
		
		*routput = output_str;
		*routput_len = output_len;
	}
	
	// $header
	if (JS_LookupProperty(jcx, global, "$header", &result) == JS_TRUE && JSVAL_IS_OBJECT(result) && !JSVAL_IS_NULL(result)) {
		header = JS_ValueToString(jcx, result);
		if (header != NULL) {
			// Header length and bytes
			header_len_uc = JS_GetStringLength(header);
			header_str_uc = JS_GetStringChars(header);
			header_str = JS_GetStringBytes(header);
			JS_EncodeCharacters(jcx, header_str_uc, header_len_uc, NULL, &header_len);
			*rheader = header_str;
			*rheader_len = header_len;
		}
	}
	
	return ok ? 0 : -1;
}

/*
 * Get current file and line
 */
JSBool ngx_javascript_global_file_line(JSContext *jcx, const char **file, int *line) {
	JSStackFrame *fp = NULL;
	JSScript *script;
	const char *rfile;
	int rline;
	do {
		JS_FrameIterator(jcx, &fp);
	} while (fp && !JS_GetFramePC(jcx, fp));
	if (fp == NULL) return JS_FALSE;
	script = JS_GetFrameScript(jcx, fp);
	if (script == NULL) return JS_FALSE;
	rfile = JS_GetScriptFilename(jcx, script);
	if (rfile == NULL || *rfile == '\0') rfile = "-";
	rline = JS_PCToLineNumber(jcx, script, JS_GetFramePC(jcx, fp));
	*file = rfile;
	*line = rline;
	return JS_TRUE;
}

/*
 * Get current file
 */
JSBool ngx_javascript_global_file_string(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	JSString *str;
	const char *file;
	int line;
	if (ngx_javascript_global_file_line(jcx, &file, &line) == JS_TRUE) {
		str = JS_NewStringCopyN(jcx, file, strlen(file));
		if (str != NULL) {
			*rval = STRING_TO_JSVAL(str);
		}
	}
	return JS_TRUE;
}

/*
 * Get current line
 */
JSBool ngx_javascript_global_line_string(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	JSString *str;
	const char *file;
	int line;
	if (ngx_javascript_global_file_line(jcx, &file, &line) == JS_TRUE) {
		str = JS_ValueToString(jcx, INT_TO_JSVAL(line));
		if (str != NULL) {
			*rval = STRING_TO_JSVAL(str);
		}
	}
	return JS_TRUE;
}

/*
 * JavaScript function executed periodically to check script execution timeout
 */
JSBool ngx_javascript_timeout(JSContext *jcx) {
	ngx_javascript_client_t *client = JS_GetContextPrivate(jcx);
	if (client == NULL) return JS_FALSE;
	if (client->start == 0) return JS_FALSE;
	time_t now = time(NULL);
	if (now - client->start > client->server->timeout) return JS_FALSE;
	return JS_TRUE;
}

/*
 * JavaScript report out of memory
 */
JSBool ngx_javascript_outofmemory(JSContext *jcx) {
	JS_ReportOutOfMemory(jcx);
	return JS_FALSE;
}

/*
 * JavaScript error handler
 */
void ngx_javascript_global_error(JSContext *jcx, const char *message, JSErrorReport *report) {
	if (report->filename && report->lineno) {
		fprintf(stderr, "%s:%i: %s\n", report->filename ? report->filename : "-", (unsigned int) report->lineno, message);
	} else {
		fprintf(stderr, "%s\n", message);
	}
	fflush(stderr);
}

/*
 * JavaScript log handler
 */
void ngx_javascript_log(JSContext *jcx, const char *message) {
	fprintf(stderr, "%s\n", message);
	fflush(stderr);
}

/*
 * JavaScript alert handler
 */
JSBool ngx_javascript_global_alert(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	JSString *str;
	if (argc < 1) return JS_TRUE;
	str = JS_ValueToString(jcx, argv[0]);
	if (str == NULL) return JS_TRUE;
	fprintf(stderr, "alert: %s\n", JS_GetStringBytes(str));
	fflush(stderr);
	return JS_TRUE;
}

/*
 * JavaScript print handler
 */
JSBool ngx_javascript_global_print(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	JSObject *global;
	JSString *old, *str, *output;
	jsval result;
	unsigned int i;
	if (argc < 1) return JS_TRUE;
	global = JS_GetGlobalObject(jcx);
	if (JS_LookupProperty(jcx, global, "$output", &result) == JS_TRUE && JSVAL_IS_STRING(result)) {
		output = JSVAL_TO_STRING(result);
	} else {
		output = JS_NewGrowableString(jcx, NULL, 0);
		if (output == NULL) return ngx_javascript_outofmemory(jcx);
		result = STRING_TO_JSVAL(output);
		JS_DefineProperty(jcx, global, "$output", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	for (i = 0; i < argc; i ++) {
		old = JS_ValueToString(jcx, argv[i]);
		if (old == NULL) continue;
		str = JS_ConcatStrings(jcx, output, old);
		if (str == NULL) {
			return ngx_javascript_outofmemory(jcx);
		} else if (str != output) {
			output = str;
			result = STRING_TO_JSVAL(output);
			JS_DefineProperty(jcx, global, "$output", result, NULL, NULL, JSPROP_ENUMERATE);
		}
	}
	return JS_TRUE;
}

/*
 * JavaScript sleep handler
 */
JSBool ngx_javascript_global_sleep(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	int t;
	if (argc < 1) return JS_TRUE;
	if (JS_ValueToInt32(jcx, argv[0], &t) == JS_TRUE && t > 0) sleep(t);
	return JS_TRUE;
}

/*
 * JavaScript usleep handler
 */
JSBool ngx_javascript_global_usleep(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	int t;
	if (argc < 1) return JS_TRUE;
	if (JS_ValueToInt32(jcx, argv[0], &t) == JS_TRUE && t > 0) usleep(t);
	return JS_TRUE;
}

/*
 * Result headers as string
 */
JSBool ngx_javascript_global_header_toString(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	JSString *s;
	size_t l, length = 0;
	jsuint i;
	jschar *c, *p;
	jsid id;
	jsval rid, result;
	JSIdArray *ids = JS_Enumerate(jcx, obj);
	if (ids != NULL) {
		for (i = 0; i < ids->length; i ++) {
			id = ids->vector[i];
			if (JS_LookupPropertyById(jcx, obj, id, &result) == JS_TRUE && JSVAL_IS_STRING(result)) {
				length += JS_GetStringLength(JSVAL_TO_STRING(result)) + 1;
				if (JS_IdToValue(jcx, id, &rid) == JS_TRUE && JSVAL_IS_STRING(rid)) {
					length += JS_GetStringLength(JSVAL_TO_STRING(rid)) + 2;
				}
			}
		}
		length ++;
		p = c = JS_malloc(jcx, length * sizeof(jschar));
		if (p == NULL) {
			JS_DestroyIdArray(jcx, ids);
			return JS_FALSE;
		}
		for (i = 0; i < ids->length; i ++) {
			id = ids->vector[i];
			if (JS_LookupPropertyById(jcx, obj, id, &result) == JS_TRUE && JSVAL_IS_STRING(result)) {
				if (JS_IdToValue(jcx, id, &rid) == JS_TRUE && JSVAL_IS_STRING(rid)) {
					s = JSVAL_TO_STRING(rid);
					l = JS_GetStringLength(s);
					if (l > 0) {
						memcpy(p, JS_GetStringChars(s), l * sizeof(jschar));
						p += l;
						*p++ = ':';
						*p++ = ' ';
					}
				}
				s = JSVAL_TO_STRING(result);
				l = JS_GetStringLength(s);
				memcpy(p, JS_GetStringChars(s), l * sizeof(jschar));
				p += l;
				*p++ = '\n';
			}
		}
		*p++ = '\n';
		s = JS_NewUCString(jcx, c, p - c);
		*rval = STRING_TO_JSVAL(s);
		JS_DestroyIdArray(jcx, ids);
	}
	return JS_TRUE;
}

/*
 * Report private error
 */
JSBool ngx_javascript_private_error(JSContext *jcx) {
	JS_ReportError(jcx, "%s", "Private error");
	return JS_FALSE;
}

/*
 * Report JavaScript error
 */
JSBool ngx_javascript_error(JSContext *jcx, const char *message) {
	JS_ReportError(jcx, "%s", message);
	return JS_FALSE;
}

/*
 * Allocate memory and return pointer to it saving information about this memory in worker
 */
void *ngx_javascript_worker_allocate(ngx_javascript_worker_t *worker, int size) {
	ngx_javascript_memory_t *r = ngx_javascript_malloc(size + sizeof(ngx_javascript_memory_t *));
	if (r == NULL) return NULL;
	r->next = NULL;
	if (worker->endmem == NULL) {
		worker->endmem = worker->memory = r;
	} else {
		worker->endmem->next = r;
		worker->endmem = r;
	}
	return r->data;
}
